# 一、我们写的 JSX 终将变成什么
万物始于 jsx,想要深入学习 react ,就应该从 jsx 入手。弄清楚 jsx ,方便学习掌握以下内容:
- 了解常用的元素会被 React 处理成什么,有利于后续理解 react fiber 类型;
- 理解 jsx 的编译过程,方便操纵 children、控制 React 渲染,有利于便捷使用 React 插槽组件。
我写了一段 react JSX 代码,接下来,我们一步步看看它最后会变成什么样子。
const toLearn = [ 'react' , 'vue' , 'webpack' , 'nodejs' ]
const TextComponent = ()=> <div> hello , i am function component </div>
class Index extends React.Component{
status = false /* 状态 */
renderFoot=()=> <div> i am foot</div>
render(){
/* 以下都是常用的jsx元素节 */
return <div style={{ marginTop:'100px' }} >
{ /* element 元素类型 */ }
<div>hello,world</div>
{ /* fragment 类型 */ }
<React.Fragment>
<div> 👽👽 </div>
</React.Fragment>
{ /* text 文本类型 */ }
my name is alien
{ /* 数组节点类型 */ }
{ toLearn.map(item=> <div key={item} >let us learn { item } </div> ) }
{ /* 组件类型 */ }
<TextComponent/>
{ /* 三元运算 */ }
{ this.status ? <TextComponent /> : <div>三元运算</div> }
{ /* 函数执行 */ }
{ this.renderFoot() }
<button onClick={ ()=> console.log( this.render() ) } >打印render后的内容</button>
</div>
}
}

# 1 babel 处理后的样子
首先,看一下上述例子中的 jsx 模版会被babel编译成什么?

和如上看到的一样,我写的 JSX 元素节点会被编译成 React Element 形式。那么,我们首先来看一下 React.createElement 的用法。
React.createElement(
type,
[props],
[...children]
)
createElement 参数:
- 第一个参数:如果是组件类型,会传入组件对应的类或函数;如果是 dom 元素类型,传入 div 或者 span 之类的字符串。
- 第二个参数:一个对象,在 dom 类型中为标签属性,在组件类型中为 props 。
- 其他参数:依次为 children,根据顺序排列。
举个例子:
<div>
<TextComponent />
<div>hello,world</div>
let us learn React!
</div>
上面的代码会被 babel 先编译成:
React.createElement("div", null,
React.createElement(TextComponent, null),
React.createElement("div", null, "hello,world"),
"let us learn React!"
)
|--------问与答--------|
问:老版本的 React 中,为什么写 jsx 的文件要默认引入 React? 如下
import React from 'react'
function Index(){
return <div>hello,world</div>
}
答:因为 jsx 在被 babel 编译后,写的 jsx 会变成上述 React.createElement 形式,所以需要引入 React,防止找不到 React 引起报错。
|---------end---------|
# 2 createElement 处理后的样子
然后点击按钮,看一下写的 demo 会被 React.createElement 变成什么:

从上面写的 jsx 结构来看,外层的 div 被 react.createElement 转换成 react element 对象,div 里面的 8 个元素分别转换成 children 子元素列表。下面就是 jsx 的转换规则,请一定要记住,以便后续能更流畅地使用 jsx 语法。
jsx元素类型 | react.createElement 转换后 | type 属性 |
|---|---|---|
element元素类型 | react element类型 | 标签字符串,例如 div |
fragment类型 | react element类型 | symbol react.fragment类型 |
| 文本类型 | 直接字符串 | 无 |
| 数组类型 | 返回数组结构,里面元素被react.createElement转换 | 无 |
| 组件类型 | react element类型 | 组件类或者组件函数本身 |
| 三元运算 / 表达式 | 先执行三元运算,然后按照上述规则处理 | 看三元运算返回结果 |
| 函数执行 | 先执行函数,然后按照上述规则处理 | 看函数执行返回结果 |
# 3 React 底层调和处理后,终将变成什么?
最终,在调和阶段,上述 React element 对象的每一个子节点都会形成一个与之对应的 fiber 对象,然后通过 sibling、return、child 将每一个 fiber 对象联系起来。
所以,我们有必要先来看一下 React 常用的 fiber 类型,以及 element 对象和 fiber 类型的对应关系。
# 不同种类的 fiber Tag
React 针对不同 React element 对象会产生不同 tag (种类) 的fiber 对象。首先,来看一下 tag 与 element 的对应关系:
export const FunctionComponent = 0; // 函数组件
export const ClassComponent = 1; // 类组件
export const IndeterminateComponent = 2; // 初始化的时候不知道是函数组件还是类组件
export const HostRoot = 3; // Root Fiber 可以理解为根元素 , 通过reactDom.render()产生的根元素
export const HostPortal = 4; // 对应 ReactDOM.createPortal 产生的 Portal
export const HostComponent = 5; // dom 元素 比如 <div>
export const HostText = 6; // 文本节点
export const Fragment = 7; // 对应 <React.Fragment>
export const Mode = 8; // 对应 <React.StrictMode>
export const ContextConsumer = 9; // 对应 <Context.Consumer>
export const ContextProvider = 10; // 对应 <Context.Provider>
export const ForwardRef = 11; // 对应 React.ForwardRef
export const Profiler = 12; // 对应 <Profiler/ >
export const SuspenseComponent = 13; // 对应 <Suspense>
export const MemoComponent = 14; // 对应 React.memo 返回的组件
# jsx 最终形成的 fiber 结构图
最终写的 jsx 会变成如下格式:

fiber 对应关系
- child: 一个由父级 fiber 指向子级 fiber 的指针。
- return:一个子级 fiber 指向父级 fiber 的指针。
- sibling: 一个 fiber 指向下一个兄弟 fiber 的指针。
温馨提示:
- 对于上述在 jsx 中写的 map 数组结构的子节点,外层会被加上 fragment ;
- map 返回数组结构,作为 fragment 的子节点。
# 二、进阶实践-可控性 render
上面的 demo 暴露出了如下问题:
- 返回的
children虽然是一个数组,但是数组里面的数据类型却是不确定的,有对象类型( 如ReactElement) ,有数组类型(如map遍历返回的子节点),还有字符串类型(如文本); - 无法对 render 后的 React element 元素进行可控性操作。
针对上述问题,我们需要对demo项目进行改造处理,具体过程可以分为4步:
将上述children扁平化处理,将数组类型的子节点打开 ;
干掉children中文本类型节点;
向children最后插入
say goodbye
元素;
克隆新的元素节点并渲染。
希望通过这个实践 demo ,大家可以加深对 jsx 编译后结构的认识,学会对 jsx 编译后的 React.element 进行一系列操作,达到理想化的目的,以及熟悉 React API 的使用。
由于,我们想要把 render 过程变成可控的,因此需要把上述代码进行改造。
class Index extends React.Component{
status = false /* 状态 */
renderFoot=()=> <div> i am foot</div>
/* 控制渲染 */
controlRender=()=>{
const reactElement = (
<div style={{ marginTop:'100px' }} className="container" >
{ /* element 元素类型 */ }
<div>hello,world</div>
{ /* fragment 类型 */ }
<React.Fragment>
<div> 👽👽 </div>
</React.Fragment>
{ /* text 文本类型 */ }
my name is alien
{ /* 数组节点类型 */ }
{ toLearn.map(item=> <div key={item} >let us learn { item } </div> ) }
{ /* 组件类型 */ }
<TextComponent/>
{ /* 三元运算 */ }
{ this.status ? <TextComponent /> : <div>三元运算</div> }
{ /* 函数执行 */ }
{ this.renderFoot() }
<button onClick={ ()=> console.log( this.render() ) } >打印render后的内容</button>
</div>
)
console.log(reactElement)
const { children } = reactElement.props
/* 第1步 : 扁平化 children */
const flatChildren = React.Children.toArray(children)
console.log(flatChildren)
/* 第2步 : 除去文本节点 */
const newChildren :any= []
React.Children.forEach(flatChildren,(item)=>{
if(React.isValidElement(item)) newChildren.push(item)
})
/* 第3步,插入新的节点 */
const lastChildren = React.createElement(`div`,{ className :'last' } ,`say goodbye`)
newChildren.push(lastChildren)
/* 第4步:修改容器节点 */
const newReactElement = React.cloneElement(reactElement,{} ,...newChildren )
return newReactElement
}
render(){
return this.controlRender()
}
}
第 1 步:React.Children.toArray 扁平化,规范化 children 数组。
const flatChildren = React.Children.toArray(children)
console.log(flatChildren)
React.Children.toArray 可以扁平化、规范化 React.element 的 children 组成的数组,只要 children 中的数组元素被打开,对遍历 children 很有帮助,而且 React.Children.toArray 还可以深层次 flat 。
打印结果:

第 2 步:遍历 children ,验证 React.element 元素节点,除去文本节点。
const newChildren :any= []
React.Children.forEach(flatChildren,(item)=>{
if(React.isValidElement(item)) newChildren.push(item)
})
用 React.Children.forEach 去遍历子节点,如果是 react Element 元素,就添加到新的 children 数组中,通过这种方式过滤掉非 React element 节点。React.isValidElement 这个方法可以用来检测是否为 React element 元素,接收一个参数——待验证对象,如果是返回 true , 否则返回 false 。
这里可能会有一个疑问就是如下:
难道用数组本身方法 filter 过滤不行么 ? 为什么要用 React.Children.forEach 遍历?
这种情况下,是完全可以用数组方法过滤的,因为 React.Children.toArray 已经处理了 children ,使它变成了正常的数组结构 也就是说 React.Children.forEach = React.Children.toArray + Array.prototype.forEach。
React.Children.forEach 本身就可以把 children 扁平化了,也就是上述第一步操作多此一举了。为什么要有第一步,主要是更多的学习一下 React api。
第 3 步:用 React.createElement ,插入到 children 最后
/* 第三步,插入新的节点 */
const lastChildren = React.createElement(`div`,{ className :'last' } ,`say goodbye`)
newChildren.push(lastChildren)
上述代码实际等于用 JSX 这么写:
newChildren.push(<div className="last" >say goodbye</div>)
第 4 步: 已经修改了 children,现在做的是,通过 cloneElement 创建新的容器元素。
为什么要用 React.cloneElement ,createElement 把上面写的 jsx,变成 element 对象; 而 cloneElement 的作用是以 element 元素为样板克隆并返回新的 React element 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。
这里 React.cloneElement 做的事情就是,把 reactElement 复制一份,再用新的 children 属性,从而达到改变 render 结果的目的。
/* 第 4 步:修改容器节点 */
const newReactElement = React.cloneElement(reactElement,{} ,...newChildren )
效果

验证 :
- ① children 已经被扁平化。
- ② 文本节点
my name is alien已经被删除。 - ③
<div className="last" > say goodbye</div>元素成功插入。
达到了预期效果。
|--------问与答--------|
问: React.createElement 和 React.cloneElement 到底有什么区别呢?
答: 可以完全理解为,一个是用来创建 element 。另一个是用来修改 element,并返回一个新的 React.element 对象。
|---------end---------|
# 三、总结
本章节主要讲到了两方面的知识。
一方面,我们写的 JSX 会先转换成 React.element,再转化成 React.fiber 的过程。这里要牢牢记住 jsx 转化成 element 的处理逻辑,还有就是 element 类型与转化成 fiber 的 tag 类型的对应关系。这对后续的学习会很有帮助。
另一方面,通过学习第一个实践 demo,我们掌握了如何控制经过 render 之后的 React element 对象。
下一章节,我们将从React组件角度出发,全方面认识React组件。
← 前言 基础篇 起源 Component →